package stocc.parser;

import java.io.BufferedReader;

import java.io.IOException;
import java.io.StreamTokenizer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import stocc.logger.Level;
import stocc.logger.Logger;
import stocc.subject.Member;
import stocc.subject.Method;
import stocc.subject.Subject;
import stocc.subject.View;
import stocc.subject.Visibility;
import stocc.subject.settings.ConversionSettings;
import stocc.subject.settings.OnNameConflict;


public class CodeParser {
	
	public CodeParser(){}

	/**
	 * 
	 * @param br A buffered reader for the stream to be parsed 
	 * @param subject The subject to be updated
	 */
	@SuppressWarnings("static-access")
	public void parse(BufferedReader br, Subject subject) throws ParsingErrorException{
		log("started", Level.DEBUG);
		
		//Creat a new view to save all the parsed data
		View view = new View();

		//Setup the keywords
		Set<String> visibilities = new HashSet<String>();
		visibilities.add("public");
		visibilities.add("protected");
		visibilities.add("private");
		Set<String> modifiers = new HashSet<String>();
		modifiers.add("static");
		modifiers.add("final");
		modifiers.add("abstract");
		modifiers.add("synchronized");
		Set<String> constructs = new HashSet<String>();
		constructs.add("class");
		constructs.add("interface");
		Set<String> inheritance = new HashSet<String>();
		inheritance.add("extends");
		inheritance.add("implements");
		
		// Setup the stream tokenizer
		// We want to parse both c (/*) and c++ (//) comments,
		// but not EOL or numbers.
		StreamTokenizer tokenizer = new StreamTokenizer(br);
		tokenizer.slashSlashComments(true);
		tokenizer.slashStarComments(true);
		tokenizer.eolIsSignificant(false);
		//tokenizer.wordChars('(',')');
		tokenizer.wordChars('=','=');
		tokenizer.wordChars('"', '"');
		

		//Setup the state machine
		State state = State.START;

		//Parse until the stream ends
		Visibility visibility = Visibility.PROTECTED;
		List<String> modifier = new ArrayList<String>();
		Method method = new Method();
		ConversionSettings settings = new ConversionSettings();
		int currDepth = 0;
		int blockDepth = 0;
		String name = null;
		String type = null;
		String block = "";
		//String pack = "default";
		
		int tokenType;
		String message; //For logging
		while(state != State.END){
			//Get the next token from the stream
			try{
				tokenType = tokenizer.nextToken();
			}catch(IOException e){
				message = "IOException thrown with message: " + e.getMessage();
				log(message, Level.CRITICAL);
				throw new ParsingErrorException(message);
			}	//Check the token type and deal with it accordingly.
			switch(tokenType){
				case(StreamTokenizer.TT_WORD):
					if(visibilities.contains(tokenizer.sval)){
						if(state == State.START){
							log("Parsed visibility: " + tokenizer.sval, Level.DEBUG);
							visibility = Visibility.valueOf(tokenizer.sval.toUpperCase());
							state = State.DECLARATION;
							modifier.clear();
						}else{
							log("Visibility found in wrong position, aborting." + tokenizer.sval, Level.CRITICAL);
							state = State.END;
						}
					}
					else if(modifiers.contains(tokenizer.sval)){
						if(state == State.START){
							log("Visibility missing from declaration, setting to 'protected'.", Level.DEBUG);
							state = State.DECLARATION;
						}
						log("Parsed modifier: " + tokenizer.sval, Level.DEBUG);
						modifier.add(tokenizer.sval);
					}
					else if(constructs.contains(tokenizer.sval)){
						if(state == State.START){
							log("Visibility missing from declaration, setting to 'protected'.", Level.DEBUG);
							state = State.DECLARATION;
						}
						log("Parsed construct: " + tokenizer.sval, Level.DEBUG);
						//view.setVisibility(Visibility.valueOf(visibility.toUpperCase()));
					}
					else if(inheritance.contains(tokenizer.sval)){
						log("Parsed inheritance: " + tokenizer.sval, Level.DEBUG);
					}
					else if(tokenizer.sval.equals("package")){
						log("Parsed package definition: " + tokenizer.sval, Level.DEBUG);
						//Get the next token from the stream
//						try{
//							tokenType = tokenizer.nextToken();
//						}catch(IOException e){
//							message = "IOException thrown with message: " + e.getMessage();
//							log(message, Level.CRITICAL);
//							throw new ParsingErrorException(message);
//						}
//						if(tokenType != StreamTokenizer.TT_WORD){
//							message = "Package name missing!";
//							log(message, Level.CRITICAL);
//							throw new ParsingErrorException(message);
//						}
//						view.setPackage(tokenizer.sval);
						state = State.PACKAGE;
					}
					else if(tokenizer.sval.equals("import")){
						log("Parsed import declaration.", Level.DEBUG);
						state = State.IMPORT;
					}
					else{
						log("Parsed word: " + tokenizer.sval, Level.DEBUG);
						switch(state){
							case BLOCK:
								//TODO Block parsing goes here.
								break;
							case ANNOTATION:
								log("Parsing annotation.",Level.DEBUG);
								//TODO parse annotations generally
								if(tokenizer.sval.equals("\"MERGE\"")){
									log("Annotation value: "+tokenizer.sval+" parsed.",Level.DEBUG);
									settings.onNameConflict = OnNameConflict.MERGE;
								}
								else if(tokenizer.sval.equals("\"DIE\"")){
									log("Annotation value: "+tokenizer.sval+" parsed.",Level.DEBUG);
									settings.onNameConflict = OnNameConflict.DIE;
								}
								else if(tokenizer.sval.equals("\"OVERRIDE\"")){
									log("Annotation value: "+tokenizer.sval+" parsed.",Level.DEBUG);
									settings.onNameConflict = OnNameConflict.OVERRIDE;
								}
								else{
									log("Annotation not recognized.",Level.DEBUG);
								}
								break;
							case IMPORT:
								log("Parsing import.",Level.DEBUG);
								if(name == null){
									name = tokenizer.sval;
								}else{
									log("Import statement too long, aborting.",Level.CRITICAL);
									state = State.END;
								}
							case DECLARATION:
								if(name != null && name.equals(type)){
									name = tokenizer.sval;
								}
								else if(type == null){
									type = tokenizer.sval;
									name = tokenizer.sval;
								}
								else{
									log("Too many words in declaration, breaking.", Level.CRITICAL);
									state = State.END;
								}
								break;
							case PARAMETER:
								if(type == null){
									log("Parsed parameter type.",Level.DEBUG);
									type = tokenizer.sval;
								}else if(name == null){
									log("Parsed parameter name.",Level.DEBUG);
									name = tokenizer.sval;
								}else{
									log("Parameter error, aborting.",Level.CRITICAL);
									state = State.END;
								}
								break;
							case PACKAGE:
								name = tokenizer.sval;
								break;
							default:
								log("Word parsed out of context, aborting.",Level.CRITICAL);
								state = State.END;
						}
					}
					break;
				case(StreamTokenizer.TT_EOF):
					message = "Parsed EOF, closing...";
					log(message, Level.INFO);
					state = State.END;
					break;
				case('@'):
					message = "Parsing @.";
					log(message, Level.DEBUG);
					state = State.ANNOTATION;
					break;
				case(';'):
					message = "Parsing semicolon.";
					log(message, Level.DEBUG);
					switch(state){
						case BLOCK:
							//TODO Add semicolon to block (newline?)
							break;
						case DECLARATION:
							log("Finished parsing member, adding to view.",Level.DEBUG);
							view.addMember(new Member(name, visibility, type));
							state = State.START;
							break;
						case PACKAGE:
							log("Parsed package definition, adding to view.",Level.DEBUG);
							view.setPackage(name);
							state = State.START;
							break;
						case IMPORT:
							log("Parsed import, adding to view.",Level.DEBUG);
							view.addImport(name);
							state = State.START;
							break;
						case PARAMETER:
							log("Parsed Method declaration, adding to view.",Level.DEBUG);
							view.addMethod(method);
							state = State.START;
							break;
						case START:
							log("Parsed lone semicolon, ignoring",Level.DEBUG);
							break;
						default:
							log("Parsed semicolon out of context, aborting.",Level.CRITICAL);
							state = State.END;
					}
					name = null;
					type = null;
					visibility = Visibility.PROTECTED;
					break;
				case('{'):
					message = "Parsing left bracket.";
					log(message, Level.DEBUG);
					switch(state){
						case BLOCK:
							log("Going one step deeper.",Level.DEBUG);
							currDepth++;
							//TODO add left bracket to block
							break;
						case DECLARATION:
							//TODO Parse arrays and other non-method block users
							log("Finished parsing view declaration, updating.",Level.DEBUG);
							//log("Name="+name,Level.DEBUG);
							view.setName(name);
							view.setVisibility(visibility);
							view.setSettings(settings);
							currDepth++;
							state = State.START;
							break;
						case PARAMETER:
							log("Parsed Method declaration, parsing code block.",Level.DEBUG);
							//method = new Method(visibility, type, name);
							//method.addParameter(paramType+" "+paramName);
							blockDepth = currDepth;
							currDepth++;
							state = State.BLOCK;
							
							//Read and add code block to method
							try{
								br.mark(500);
								block = "";
								int blockToken = br.read();
								while(blockToken != (int) '}'){
									block += (char) blockToken;
									blockToken = br.read();
								}
								br.reset();
								log("Parsed code block", Level.DEBUG);
							}catch (IOException e){
								log("Code block extraction failed!", Level.CRITICAL);
							}
							break;
						default:
							log("Parsed left bracket out of context, aborting.",Level.CRITICAL);
							state = State.END;
					}
					name = null;
					type = null;
					visibility = Visibility.PROTECTED;
					break;
				case('}'):
					message = "Parsing right bracket.";
					log(message, Level.DEBUG);
					switch(state){
						case BLOCK:
							currDepth--;
							if(currDepth == blockDepth){
								log("Finished parsing code block, adding to method.",Level.DEBUG);
								method.setCode(block);
								view.addMethod(method);
								method = new Method();
								blockDepth = 0;
								state = State.START;
							}
							else if(currDepth > blockDepth){
								//We are still inside the block
								//TODO Add right bracket to code block
							}
							else{
								log("Unmatched right bracket parsed, aborting.",Level.CRITICAL);
								state = State.END;
							}
							break;
						case START:
							log("Parsing finished, returning.",Level.INFO);
							currDepth--;
							log("Current depth is "+currDepth+" (should be 0).",Level.DEBUG);
							//TODO Use depth to measure success?
							state = State.END;
							break;
						default:
							log("Unmatched right bracket parsed, aborting.",Level.CRITICAL);
							state = State.END;
					}
					break;
				case('('):
					message = "Parsing left parenthesis.";
					log(message, Level.DEBUG);
					switch(state){
						case BLOCK:
							//TODO Add left bracket to block
							break;
						case ANNOTATION:
							break;
						case DECLARATION:
							log("Parsed method declaration, parsing parameters.",Level.DEBUG);
							method.setVisibility(visibility);
							method.setReturnType(type);
							method.setName(name);
							state = State.PARAMETER;
							break;
						default:
							log("Left parenthesis parsed out of context, aborting.",Level.CRITICAL);
							state = State.END;
					}
					name = null;
					type = null;
					visibility = Visibility.PROTECTED;
					break;
				case(')'):
					message = "Parsing right parenthesis.";
					log(message, Level.DEBUG);
					switch(state){
						case BLOCK:
							//TODO Add right parenthesis to block
							break;
						case ANNOTATION:
							log("Parsed annotation.",Level.DEBUG);
							state = State.START;
							break;
						case PARAMETER:
							log("Parameter parsed, adding to method.",Level.DEBUG);
							if(type != null && name != null){
								method.addParameter(type+" "+name);
							}else{
								log("No parameters parsed.",Level.DEBUG);
							}
							break;
						default:
							log("Right parenthesis parsed out of context, aborting.",Level.CRITICAL);
							state = State.END;
					}
					name = null;
					type = null;
					visibility = Visibility.PROTECTED;
					break;
				case(','):
					message = "Parsing comma.";
					log(message, Level.DEBUG);
					switch(state){
						case BLOCK:
							//TODO Add comma to block
							break;
						case PARAMETER:
							log("Parameter parsed, adding to method.",Level.DEBUG);
							if(type != null && name != null){
								method.addParameter(type+" "+name);
							}else{
								log("Parameter error, aborting.",Level.CRITICAL);
								state = State.END;
							}
							break;
						default:
							log("Comma parsed out of context, aborting.",Level.CRITICAL);
							state = State.END;
					}
					name = null;
					type = null;
					visibility = Visibility.PROTECTED;
					break;
				case(StreamTokenizer.TT_NUMBER):
					message = "Parsing failed: Number encountered (we shouldn't parse numbers).";
					log("Throwing ParsingErrorException with message: " + message, Level.CRITICAL);
					throw new ParsingErrorException(message);
				case(StreamTokenizer.TT_EOL):
					message = "Parsing failed: EOL encountered (we shouldn't parse endlines).";
					log("Throwing ParsingErrorException with message: " + message, Level.CRITICAL);
					throw new ParsingErrorException(message);
				default:
					message = "Parsing unknown: ttype = " + tokenizer.ttype;
					log(message, Level.DEBUG);
					if(tokenizer.sval != null){
						log(tokenizer.sval, Level.DEBUG);
					}
					log("Continuing...", Level.DEBUG);
					//throw new ParsingErrorException(message);
			}
		}
		subject.addView(view);
	}
	/**
	 * Used to notify the user of errors encountered during parsing.
	 */
	public static class ParsingErrorException extends Exception{
		final static long serialVersionUID = 1;
		/**
		 * @param reason The describing message.
		 */
		public ParsingErrorException(String reason) {
			super(reason);
		}
	}
	
	private enum State {
		START,
		PACKAGE,
		IMPORT,
		ANNOTATION,
		DECLARATION,
		PARAMETER,
		BLOCK,
		END,
	}
	
	/**
	 * Logging method, add this to any class that does logging
	 * @param message The message to be logged
	 * @param level The logging level of the message
	 */
	private static void log(String message, Level level){
		Logger.getLogger().log(level, "CodeParser", message);
	}	
}
